package com.sunyuming;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.MessageToMessageDecoder;
import io.netty.handler.codec.MessageToMessageEncoder;
import io.netty.handler.codec.http.*;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.*;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.net.*;
import java.nio.charset.Charset;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class HttpServerJob {

    public static AtomicInteger open = new AtomicInteger(0);
    public static AtomicInteger close = new AtomicInteger(0);

    public static String clientP12default = "/Users/mac/work/MyTest/MyHttps/src/main/resources/mkcert/myhost.com.p12::changeit";
    public static String clientP12 = clientP12default;

    public static class ReqQueue {

        private BlockingQueue<OriginHttp> arrayBlockingQueue = new LinkedBlockingDeque<OriginHttp>(100);

        public void startConsumer(Boolean shortConnection) {
            for(int i=0; i<10; ++i) {
                new Thread(new Customer(shortConnection)).start();
            }
        }

        public void put(OriginHttp originHttp) {
            try {
                arrayBlockingQueue.put(originHttp);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        private class Customer implements Runnable {

            private Boolean shortConnection;
            public Customer(Boolean shortConnection) {
                this.shortConnection = shortConnection;
            }

            @Override
            public void run() {
                try {
                    while (true){
                        OriginHttp originHttp = arrayBlockingQueue.take();
                        originHttp.print();
                        ChannelFuture channelFuture = ProxySender.send(shortConnection, originHttp, originHttp.getUrl());
//                        if(shortConnection)
//                            channelFuture.channel().close();
                        Thread.sleep(100);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

    }

    public static class BodyToResponseEncoder extends MessageToMessageEncoder<ResHttp> {

        @Override
        protected void encode(ChannelHandlerContext channelHandlerContext, ResHttp resHttp, List<Object> list) throws Exception {

            FullHttpResponse response = new DefaultFullHttpResponse(
                    HttpVersion.HTTP_1_1,
                    HttpResponseStatus.valueOf(resHttp.getRet()),
                    Unpooled.wrappedBuffer(resHttp.getBody()));

            Map<ResHttp.HeaderName, String> headers = resHttp.getHeaders();
            for (Map.Entry<ResHttp.HeaderName, String> entry : headers.entrySet()) {
                response.headers().add(entry.getKey().getName(), entry.getValue());

                if (entry.getKey().getName().equals("Set-Cookie")) {
                    // response.headers().set(entry.getKey(), entry.getValue().replace("Secure", "").replace("HttpOnly", ""));

                    //cookieNoUse = entry.getValue();
                    //cookies.put(resHttp.getPort(), entry.getValue());
                }
            }

            response.headers().add("Set-Cookie", "ssid=hhh;");
            //response.headers().remove("X-Frame-Options");
            response.headers().set("testproxy", "hhh");

            /**
             * one of content_length and chunked in response header neccesary in http long connections
             */
//            if(!response.headers().contains("Transfer-Encoding") && !"chunked".equals(response.headers().get("Transfer-Encoding")))
//                response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, response.content().readableBytes());

//        response.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
//        response.headers().set(HttpHeaders.Names.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
//        response.headers().set(HttpHeaders.Names.ACCESS_CONTROL_ALLOW_HEADERS, HttpHeaders.Names.CONTENT_TYPE);

            {
                response.headers().set("Connection", "keep-alive");
                response.headers().remove("Keep-alive");
            }
            {
                //   response.headers().remove("Transfer-Encoding");
            }

            // Error: Parse Error: Content-Length can't be present with chunked encoding
            if (!response.headers().contains("Transfer-Encoding") && !"chunked".equals(response.headers().get("Transfer-Encoding")))
                response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, response.content().readableBytes());
            list.add(response);
        }
    }

    public static class OriginHttp {
        private static AtomicInteger serial = new AtomicInteger(0);
        private String url;
        private String method;
        private String uri;
        private Map<String, String> headers;
        private byte[] body;
        ChannelHandlerContext context;
        private Boolean print = true;
        private String clientCert;
        private int proxy;
        private int ser;

        public OriginHttp(String method, String uri, Map<String, String> headers, byte[] body, ChannelHandlerContext context) {
            this.method = method;
            this.uri = uri;
            this.headers = headers;
            this.body = body;
            this.context = context;
            ser = serial.incrementAndGet();
        }

        public void print() {
            if (!print)
                return;

            print(null);
            print("***************************req" + ser);
            print(url);
            print(method);
            print(uri);
            for (Map.Entry<String, String> item : headers.entrySet()) {
                try {
                    print(item.getKey() + "\t" + URLDecoder.decode(item.getValue()));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            if (body != null || body.length != 0) {
                try {
                    String tmp = URLDecoder.decode(new String(body));
                    print(tmp);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            print("***************************");
            print(null);
        }

        private void print(String tmp) {
            System.out.println("========" + tmp);
        }

        public String getMethod() {
            return method;
        }

        public String getUri() {
            return uri;
        }

        public Map<String, String> getHeaders() {
            return headers;
        }

        public byte[] getBody() {
            return body;
        }

        public ChannelHandlerContext getContext() {
            return context;
        }

        public String getUrl() {
            return url;
        }

        public void setUrl(String url) {
            this.url = url;
        }

        public Boolean getPrint() {
            return print;
        }

        public int getSerial() {
            return ser;
        }

        public String getClientCert() {
            return clientCert;
        }

        public void setClientCert(String clientCert) {
            this.clientCert = clientCert;
        }
    }

    private static class ProxySender {

        public static ChannelFuture send(Boolean shortConnection, OriginHttp originHttp, String dir) {

            CloseableHttpClient httpClient = null;
            try {
                String method = originHttp.getMethod();
                String uri = originHttp.getUri();

                String url = (!uri.startsWith("http")) ? new StringBuilder(dir).append(uri).toString() : uri;

                HttpRequestBase httpUriRequest = null;

                if ("HEAD".equals(method) || "GET".equals(method)) {
                    if ("DELETE".equals(method))
                        httpUriRequest = new HttpDelete(url);
                    if ("GET".equals(method))
                        httpUriRequest = new HttpGet(url);
                    if ("HEAD".equals(method))
                        httpUriRequest = new HttpHead(url);
                } else if ("POST".equals(method) || "DELETE".equals(method) || "PUT".equals(method)) {
                    if ("POST".equals(method))
                        httpUriRequest = new HttpPost(url);
                    if ("DELETE".equals(method))
                        httpUriRequest = new MyHttpDelete(url);
                    if ("PUT".equals(method))
                        httpUriRequest = new HttpPut(url);

                    HttpEntityEnclosingRequestBase httpPost = (HttpEntityEnclosingRequestBase) httpUriRequest;
                    try {
                        byte[] body = originHttp.getBody();
                        for (Map.Entry<String, String> entry : originHttp.getHeaders().entrySet()) {
                            /**
                             * change request header here
                             */
                        }
                        /**
                         * change request body here
                         */
                        httpPost.setEntity(new ByteArrayEntity(body));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                } else {
                    throw new RuntimeException(open.intValue() + method);
                }

                Map<String, String> headers = originHttp.getHeaders();
                for (Map.Entry<String, String> entry : headers.entrySet()) {
                    String val = entry.getValue();
                    httpUriRequest.addHeader(entry.getKey(), val);
                }

                //        httpUriRequest.addHeader("Cookie", cookie);
                httpUriRequest.removeHeaders("Content-Length");
                httpUriRequest.removeHeaders("Origin");
                httpUriRequest.removeHeaders("Referer");
                httpUriRequest.removeHeaders("Host");
                httpUriRequest.addHeader("Origin", dir);
                httpUriRequest.addHeader("Referer", dir);
                /**
                 * httpclient will add length
                 */
                //httpUriRequest.setHeader("Content-Length", "");

                //    httpUriRequest.addHeader("Host", dir);

                httpClient = HttpClientFactory.createSSLClientDefault(originHttp.getClientCert(), null);
                org.apache.http.HttpResponse response = null;

                RequestConfig.Builder builder = RequestConfig.custom();
                builder.setRedirectsEnabled(false).setConnectTimeout(10000).setSocketTimeout(1000000);//下载文件需要放大

                String proxy = System.getProperty("tcpproxy");
                if (proxy != null) {
                    builder.setProxy(new HttpHost(proxy.split(":")[0], Integer.parseInt(proxy.split(":")[1])));
                }
                httpUriRequest.setConfig(builder.build());
                response = httpClient.execute(httpUriRequest);

                Map<ResHttp.HeaderName, String> map = new HashMap<>();
                Header[] headersRes = response.getAllHeaders();
                for (Header header : headersRes) {
                    String val = header.getValue();
                    /**
                     * change response header here
                     */

                    map.put(new ResHttp.HeaderName(header.getName()), val);
                }

                int ret = response.getStatusLine().getStatusCode();

                HttpEntity responseEntity = response.getEntity();
                ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                if (responseEntity != null) {
                    InputStream inputStream = responseEntity.getContent();
                    byte[] bytes = new byte[1024];
                    int i = 0;
                    while ((i = inputStream.read(bytes)) != -1) {
                        byteArrayOutputStream.write(bytes, 0, i);
                    }
                }

                byte[] body = byteArrayOutputStream.toByteArray();

                /**
                 * change response body here
                 */
                ResHttp resHttp = new ResHttp(ret, map, body, -1, originHttp.getSerial());

                resHttp.setPrint(originHttp.getPrint());
                resHttp.print();

                ChannelFuture channelFuture = originHttp.getContext().writeAndFlush(resHttp);
                channelFuture = channelFuture.addListener(new ChannelFutureListener() {
                    @Override
                    public void operationComplete(ChannelFuture future) throws Exception {

                        /**
                         * to use http short connections by this line
                         */
                        if (shortConnection)
                            future.channel().close();

                        if (!future.isSuccess()) {
                            future.cause().printStackTrace();
                            //future.channel().close();
                        }
                    }
                });
                //    channelFuture.sync();
                return channelFuture;

            } catch (Exception e) {
                e.printStackTrace();
                originHttp.context.channel().close();
            } finally {
                if (httpClient != null) {
                    try {
                        httpClient.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
            return null;
        }
    }

    private static class HttpClientFactory {

        protected static class MySec extends SecureRandom {

            int i = -1;

            public MySec(int i) {
                this.i = i;
            }

            @Override
            public void nextBytes(byte[] bytes) {
                super.nextBytes(bytes);
                if(bytes.length < 7)
                    return;
                bytes[4] = bytes[5] = 0;
                bytes[6] = (byte)i;
            }
        }

        public static CloseableHttpClient createSSLClientDefault(String clientCert, Integer index) {
            try {
                SSLContextBuilder sslContextBuilder = SSLContexts.custom()
                        .loadTrustMaterial(new TrustStrategy() {
                            @Override
                            public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                                return true;
                            }
                        });

                if (index != null) {
                    sslContextBuilder.setSecureRandom(new MySec(index));
                }

                if (clientCert != null && !"".equals(clientCert)) {
                    KeyStore keyStore = KeyStore.getInstance("PKCS12");
                    String[] auth = clientCert.split("::");
                    try {
                        keyStore.load(new FileInputStream(new File(auth[0])), auth[1].toCharArray());
                        sslContextBuilder.loadKeyMaterial(keyStore, auth[1].toCharArray());
                    } catch (Throwable e) {
                        System.out.println(e.getMessage());
                    }
                }
                SSLContext sslContext = sslContextBuilder.build();
                HostnameVerifier hostnameVerifier = NoopHostnameVerifier.INSTANCE;
                SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, hostnameVerifier);

                return HttpClients.custom().setSSLSocketFactory(sslsf).build();//setProxyAuthenticationStrategy(new ProxyAuthenticationStrategy()).setProxy(new HttpHost("webproxy.wlb2.nam.nsroot.net", 8080)).build();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return HttpClients.createDefault();
        }
    }

    private static class ResHttp {
        private int serial;
        private int ret;
        private Map<HeaderName, String> headers;
        private byte[] body;
        private Boolean print = false;

        public ResHttp(int ret, Map<HeaderName, String> headers, byte[] body, int port, int serial) {
            this.ret = ret;
            this.headers = headers;
            this.body = body;
            this.serial = serial;
        }

        public void print() {
            if (!print)
                return;

            for (Map.Entry<HeaderName, String> entry : headers.entrySet()) {
                if ("Content-Type".equals(entry.getKey().getName())) {
                    String contentType = entry.getValue();
                    if (contentType != null && -1 != contentType.indexOf("image"))
                        return;
                }
            }

            print(null);
            print("***************************res" + serial);
            try {
                print("ret " + ret);
                for (Map.Entry<HeaderName, String> item : headers.entrySet()) {
                    print(item.getKey().getName() + "\t" + item.getValue());
                }
                if (body != null && body.length != 0) {
                    String tmp = new String(body);
                    print(tmp);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            print("***************************");
            print(null);
        }

        private void print(String tmp) {
            System.out.println("========" + tmp);
        }

        public int getRet() {
            return ret;
        }

        public Map<HeaderName, String> getHeaders() {
            return headers;
        }

        public byte[] getBody() {
            return body;
        }

        public void setPrint(Boolean print) {
            this.print = print;
        }

        private static class HeaderName {
            private String name;

            public HeaderName(String name) {
                this.name = name;
            }

            @Override
            public int hashCode() {
                return super.hashCode();
            }

            @Override
            public boolean equals(Object obj) {
                return super.equals(obj);
            }

            public String getName() {
                return name;
            }

            public void setName(String name) {
                this.name = name;
            }
        }
    }

    private static class MyHttpDelete extends HttpEntityEnclosingRequestBase {
        public static final String METHOD_NAME = "DELETE";

        @Override
        public String getMethod() {
            return METHOD_NAME;
        }

        public MyHttpDelete(final String uri) {
            super();
            setURI(URI.create(uri));
        }
    }
}